#include "config.h"

#include "ecm.h"

#ifdef ECM_MTRACE

static ecm_thread_key_t	mtrace_key;

static ECMBOOL	tracing;
static ecm_uint32	trace_level;
static ecm_thread_t	tracer;
static ecm_hashtable_t	usages;
static ecm_hashtable_t	traces;
static ecm_mutex_t	mtrace_lock;
static ecm_cond_t	mtrace_cond;

static ecm_uint32	mem_used;
static ecm_uint32	mem_used_per_thread;
static ecm_uint32	ptrs_cnt;
static ecm_uint32	ptrs_cnt_per_thread;
static ecm_uint32	hash_cnt;
static ecm_uint32	hash_cnt_per_thread;
static ecm_uint32	mutex_cnt;
static ecm_uint32	mutex_cnt_per_thread;
static ecm_uint32	SRs_cnt;
static ecm_uint32	SRs_cnt_per_thread;

void
ECM_MTRACE_LOCK(void)
{
	ecm_mutex_lock(mtrace_lock);
}

void
ECM_MTRACE_UNLOCK(void)
{
	ecm_mutex_unlock(mtrace_lock);
}

void
ECM_MTRACE_BLOCK(void)
{
	ecm_uint32	*prefcnt;

	prefcnt = ecm_thread_getspecific(mtrace_key);
	if (prefcnt == NULL) {
		prefcnt = malloc(sizeof(ecm_uint32));
		*prefcnt = 0;
		ecm_thread_setspecific(mtrace_key, prefcnt);
	}
	(*prefcnt)++;
}

void
ECM_MTRACE_UNBLOCK(void)
{
	ecm_uint32	*prefcnt;

	prefcnt = ecm_thread_getspecific(mtrace_key);
	(*prefcnt)--;
	if (*prefcnt == 0) {
		ecm_thread_setspecific(mtrace_key, NULL);
		free(prefcnt);
	}
}

#define UPDATE_SIZE(item, alloc, type, size)	\
	do {					\
		if (alloc) item += size; else item -= size;	\
		if (ecm_self_thread() == tracer) {		\
			if (alloc) item ## _per_thread += size;	\
			else { \
				if (item ## _per_thread < size)	\
					item ## _per_thread = 0;	\
				else					\
					item ## _per_thread -= size;	\
			}	\
		}		\
	} while (0)

#define UPDATE_COUNT(item, alloc, type)		\
	do {					\
		if (alloc) item ++; else item --;		\
		if (ecm_self_thread() == tracer) {		\
			if (alloc) item ## _per_thread ++;	\
			else if (item ## _per_thread > 0)	\
				item ## _per_thread --;	\
		}		\
	} while (0)

static void
update_usage(ECMBOOL alloc, ecm_mtrinfo_type_t type, ecm_size_t size)
{
	switch (type) {
	case ECM_MTRINFO_MEM:
		UPDATE_SIZE(mem_used, alloc, type, size);
		break;
	case ECM_MTRINFO_PTRS:
		UPDATE_COUNT(ptrs_cnt, alloc, type);
		break;
	case ECM_MTRINFO_HASHTABLE:
		UPDATE_COUNT(hash_cnt, alloc, type);
		break;
	case ECM_MTRINFO_MUTEX:
		UPDATE_COUNT(mutex_cnt, alloc, type);
		break;
	case ECM_MTRINFO_SR:
		UPDATE_COUNT(SRs_cnt, alloc, type);
		break;
	}
}

#define SET_CALLER(LEVEL)	\
	do {		\
		if (__builtin_frame_address(LEVEL + 1))	\
			mtrinfo->callers[LEVEL] = __builtin_return_address(LEVEL + 1);	\
		else	\
			goto out_SET_CALLER; \
	} while (0)

void
ECM_MEM_TRACE(ECMBOOL alloc, ecm_mtrinfo_type_t type, ecm_cptr_t ptr, ecm_size_t size)
{
	ecm_mtrinfo_t	*mtrinfo;

	if (ptr == NULL)
		return;
	if (ecm_thread_getspecific(mtrace_key))
		return;

	ECM_MTRACE_BLOCK();
	ECM_MTRACE_LOCK();

	if (!alloc) {
		ecm_hashtable_remove(traces, sizeof(ecm_ptr_t), &ptr);
		mtrinfo = ecm_hashtable_remove(usages, sizeof(ecm_ptr_t), &ptr);
		ecm_cond_broadcast(mtrace_cond);
		if (mtrinfo) {
			update_usage(alloc, type, mtrinfo->size);
			free(mtrinfo);
		}
	}
	else {
		mtrinfo = malloc(sizeof(ecm_mtrinfo_t));
		mtrinfo->type = type;
		update_usage(alloc, type, size);
		mtrinfo->ptr = ptr;
		mtrinfo->size = size;
		memset(mtrinfo->callers, 0, sizeof(ecm_ptr_t) * ECM_MTRACE_MAX_CALLERS);
		while (!ecm_hashtable_insert(usages, mtrinfo, sizeof(ecm_ptr_t), &ptr)) {
			ecm_cond_wait(mtrace_cond, mtrace_lock, NULL);
		};

		if (!tracing) {
			ECM_MTRACE_UNLOCK();
			ECM_MTRACE_UNBLOCK();
			return;
		}
			
#ifndef _WIN32
		switch (trace_level) {
		case 15:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6); SET_CALLER(7);
			SET_CALLER(8); SET_CALLER(9);
			SET_CALLER(10);	SET_CALLER(11);
			SET_CALLER(12);	SET_CALLER(13);
			SET_CALLER(13);	SET_CALLER(14);
			SET_CALLER(15);
			break;
		case 14:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6); SET_CALLER(7);
			SET_CALLER(8); SET_CALLER(9);
			SET_CALLER(10);	SET_CALLER(11);
			SET_CALLER(12);	SET_CALLER(13);
			SET_CALLER(14);
			break;
		case 13:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6); SET_CALLER(7);
			SET_CALLER(8); SET_CALLER(9);
			SET_CALLER(10);	SET_CALLER(11);
			SET_CALLER(12); SET_CALLER(13);
			break;
		case 12:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6); SET_CALLER(7);
			SET_CALLER(8); SET_CALLER(9);
			SET_CALLER(10);	SET_CALLER(11);
			SET_CALLER(12);
			break;
		case 11:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6); SET_CALLER(7);
			SET_CALLER(8); SET_CALLER(9);
			SET_CALLER(10);	SET_CALLER(11);
			break;
		case 10:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6); SET_CALLER(7);
			SET_CALLER(8); SET_CALLER(9);
			SET_CALLER(10);
			break;
		case 9:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6); SET_CALLER(7);
			SET_CALLER(8); SET_CALLER(9);
			break;
		case 8:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6); SET_CALLER(7);
			SET_CALLER(8);
			break;
		case 7:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6); SET_CALLER(7);
			break;
		case 6:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			SET_CALLER(6);
			break;
		case 5:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4); SET_CALLER(5);
			break;
		case 4:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			SET_CALLER(4);
			break;
		case 3:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2); SET_CALLER(3);
			break;
		case 2:
			SET_CALLER(0); SET_CALLER(1);
			SET_CALLER(2);
			break;
		case 1:
			SET_CALLER(0); SET_CALLER(1);
			break;
		case 0:
			SET_CALLER(0);
			break;
		default:
			break;
		}
out_SET_CALLER:
#else
		{
			extern void SetCallTrace(ecm_mtrinfo_t *);
			SetCallTrace(mtrinfo);
		}
#endif
		if (tracer == 0 || ecm_is_self_thread(tracer))
			ecm_hashtable_insert(traces, mtrinfo, sizeof(ecm_ptr_t), &ptr);
	}
	ECM_MTRACE_UNLOCK();
	ECM_MTRACE_UNBLOCK();
}

static void
mtrace_clear(void)
{
	ecm_hashtable_clear(traces);
}

void
ecm_mtrace(ECMBOOL self_trace_mode, ecm_uint32 level)
{
	ECM_MTRACE_BLOCK();
	ECM_MTRACE_LOCK();

	if (traces)
		mtrace_clear();

	traces = ecm_hashtable_create(32);
	ecm_hashtable_set_autorehash(traces, 1);

	if (level >= ECM_MTRACE_MAX_CALLERS)
		trace_level = ECM_MTRACE_MAX_CALLERS - 1;
	else
		trace_level = level;
	tracing = ECM_TRUE;
	if (self_trace_mode)
		tracer = ecm_self_thread();
	else
		tracer = 0;
	mem_used_per_thread = 0;

	ECM_MTRACE_UNLOCK();
	ECM_MTRACE_UNBLOCK();
}

void
ecm_muntrace(void)
{
	ECM_MTRACE_LOCK();

	tracing = ECM_FALSE;
	mem_used_per_thread = 0;

	ECM_MTRACE_UNLOCK();
}

void
ecm_mtrace_clear(void)
{
	ECM_MTRACE_BLOCK();
	ECM_MTRACE_LOCK();

	tracing = ECM_FALSE;
	mtrace_clear();
	traces = NULL;

	ECM_MTRACE_UNLOCK();
	ECM_MTRACE_UNBLOCK();
}

ECMBOOL
ecm_is_mtraced(void)
{
	return tracing;
}

ecm_ptrs_t *
ecm_get_mtrinfos(void)
{
	ecm_ptrs_t	*mtrinfos;
	ecm_mtrinfo_t	*mtrinfo;
	ecm_ht_iterator_t	iterator;
	void	*p, *p_key;

	if (traces == NULL)
		return NULL;

	ECM_MTRACE_BLOCK();
	mtrinfos = ecm_ptrs_new(0);

	for (p = ecm_hashtable_first(traces, &iterator, &p_key);
	     p; p = ecm_hashtable_next(traces, &iterator, &p_key)) {
		mtrinfo = (ecm_mtrinfo_t *)malloc(sizeof(ecm_mtrinfo_t));
		memcpy(mtrinfo, p, sizeof(ecm_mtrinfo_t));
		ecm_ptrs_add(mtrinfos, mtrinfo);
	}

	ECM_MTRACE_UNBLOCK();
	return mtrinfos;
}

void
ecm_clear_mtrinfos(ecm_ptrs_t *mtrinfos)
{
	ecm_mtrinfo_t	*mtrinfo;
	ecm_uint32	i;

	ECM_MTRACE_BLOCK();
	for (i = 0; i < mtrinfos->len; i++) {
		mtrinfo = mtrinfos->pdata[i];
		free(mtrinfo);
	}
	ecm_ptrs_free(mtrinfos);
	ECM_MTRACE_UNBLOCK();
}

void
ecm_clear_mtraces(ecm_ptrs_t *mtraces)
{
	ecm_mtrace_t	*mtrace;
	ecm_uint32	i, j;

	ECM_MTRACE_BLOCK();
	for (i = 0; i < mtraces->len; i++) {
		mtrace = mtraces->pdata[i];
		for (j = 0; j < ECM_MTRACE_MAX_CALLERS; j++)
			ecm_free(mtrace->callers[j]);
		ecm_free(mtrace->meminfo);
		ecm_free(mtrace);
	}
	ecm_ptrs_free(mtraces);
	ECM_MTRACE_UNBLOCK();
}

void
ecm_get_memused(ecm_memused_t *pmemused)
{
	ECM_MTRACE_LOCK();
	pmemused->mem_used = mem_used;
	pmemused->mem_used_per_thread = mem_used_per_thread;
	pmemused->ptrs_cnt = ptrs_cnt;
	pmemused->ptrs_cnt_per_thread = ptrs_cnt_per_thread;
	pmemused->hash_cnt = hash_cnt;
	pmemused->hash_cnt_per_thread = hash_cnt_per_thread;
	pmemused->mutex_cnt = mutex_cnt;
	pmemused->mutex_cnt_per_thread = mutex_cnt_per_thread;
	ECM_MTRACE_UNLOCK();
}

ECMBOOL
ecm_mtrace_init(void)
{
	mtrace_key = ecm_thread_key_create();
	ECM_MTRACE_BLOCK();
	usages = ecm_hashtable_create(32);
	ecm_hashtable_set_autorehash(usages, ECM_TRUE);
	mtrace_lock = ecm_mutex_create(ECM_FALSE);
	mtrace_cond = ecm_cond_create();
	ECM_MTRACE_UNBLOCK();
	return ECM_TRUE;
}

#endif
